fix(control-plane): map generated ApiException to typed OtariError#21
Conversation
Control-plane resources called the generated client directly, so HTTP errors surfaced as the raw ApiException instead of a typed OtariError (the inference path already maps these in client.py). Extract the mapping into module-level map_api_exception/extract_detail helpers and route every control-plane ergonomic alias through a _translate decorator; the raw escape hatch is left unwrapped. Adds a parametrized unit test across all five resources. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Note: this comment was drafted by Claude via back-and-forth with @njbrake. The reasoning and decisions are his; the prose is Claude's. Cross-linking this to the cross-SDK side of the error-mapping work. The conformance assertion from mozilla-ai/otari#226 (a control-plane call must surface the same typed error the inference path raises, not the raw generated exception) now has a gateway-side anchor and a shared contract doc:
The test in this PR already implements the SDK side of that assertion across all five control-plane resources (keys, users, budgets, pricing, usage), mapping the generated |
Keep the PR a focused bug fix: remove the uv.lock this branch added (committing a lockfile for the first time is a project-wide decision unrelated to the error-mapping fix, and CI does not consume it), and collapse a stray extra blank line between the two new module functions in _base.py. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4bda867 to
5b88cd9
Compare
…ifest The gateway spec grew ~18 dashboard/admin management endpoints (aliases, provider-credentials, providers, settings, model metadata/discovery, usage/count, key rotate, budget reset-logs) that no SDK shell wraps yet. The endpoint-coverage drift gate fetches the live gateway spec, so these turned the gate red on every SDK independently of the error-mapping fix. Defer them under [excluded] with a "not yet wrapped" reason to restore a green gate; promote to [covered] when a shell surfaces them. Kept identical across all four SDKs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Note: this PR was drafted by Claude via back-and-forth with @njbrake. The reasoning and decisions are his; the prose is Claude's.
Description
Control-plane resources (
keys,users,budgets,pricing,usage) called the generated client directly, so any HTTP error surfaced as the rawotari._client.exceptions.ApiExceptionrather than a typedotari.errors.OtariError. The inference path already maps generated exceptions inclient.py, so the two surfaces had an inconsistent error contract: a bad master key onkeys.list()raised anApiException(and a full CLI traceback), while the inference path raised a cleanAuthenticationError.This makes the control plane match the inference path:
src/otari/_base.py: extract the existing mapping into module-levelmap_api_exceptionandextract_detailhelpers (behavior-identical move)._BaseOtariClient._map_api_exception/_extract_detailnow delegate to them, so the inference and streaming paths are unchanged.src/otari/control_plane.py: add a small_translatedecorator that catchesApiExceptionand re-raisesmap_api_exception(exc), applied to all 22 ergonomic aliases. Therawgenerated surface is intentionally left unwrapped as the escape hatch.tests/unit/test_control_plane_aliases.py: parametrized test asserting each resource alias surfaces a typedAuthenticationError(not a rawApiException) on a 401, withstatus_codeand detail preserved.No public API change: the ergonomic aliases keep their signatures and return types; only the exception type on failure changes (raw
ApiExceptionbecomes typedOtariError).PR Type
Relevant issues
Fixes #20.
Checklist
tests/unit/test_control_plane_aliases.py).uv run ruff check .,uv run mypy src/,uv run pytest tests/unit).DoD results:
ruffclean,mypy --strictclean (8 files),pytest tests/unit127 passed.AI Usage
AI Model/Tool used: Claude Code (Claude Opus 4.8)
Any additional AI details you'd like to share: Generated via back-and-forth with @njbrake; the diagnosis and decisions are his.